﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using Inovout.Descriptor;
using Inovout.Linq;

namespace System.Linq
{
    public static class QueryableWhereExtensions
    {
        public static IQueryable<TModel> Where<TModel>(this IQueryable<TModel> query, IEnumerable<FilterCriteria> filters) where TModel : class
        {
            return WhereExecutor<TModel>.Execute(query, filters);
        }
        private static class WhereExecutor<TModel> where TModel : class
        {
            private static readonly ConcurrentDictionary<FilterCachedKey, Func<IQueryable<TModel>, string[], IQueryable<TModel>>> queryCache
           = new ConcurrentDictionary<FilterCachedKey, Func<IQueryable<TModel>, string[], IQueryable<TModel>>>();
            internal static IQueryable<TModel> Execute(IQueryable<TModel> query, IEnumerable<FilterCriteria> filters)
            {
                if (filters == null || filters.Count() == 0)
                {
                    throw new ArgumentException("filters");
                }
                FilterExpression filterExpression = new FilterExpression(filters);
                if (!filterExpression.Filters.Any())
                {
                    return query;
                }
                var filterItems = filterExpression.Filters;
                FilterCachedKey key = new FilterCachedKey(typeof(TModel), filterItems);
                var queryExecutor = queryCache.GetOrAdd(key, _ =>
                {
                    return BuildWhereExpression(filterItems).Compile();
                });

                return queryExecutor(query, filterExpression.Values);
                //Expression<Func<IQueryable<TModel>, string[], IQueryable<TModel>>> queryExpression =
                //   BuildQueryExpression<TModel>(filterItems);
                //var queryFunc = queryExpression.Compile();
                //return queryFunc(query, filterExpression.Values);

            }

            private static Expression<Func<IQueryable<TModel>, string[], IQueryable<TModel>>>
                BuildWhereExpression(IEnumerable<FilterItemExpression> filters)
            {
                Type entityType = typeof(TModel);
                Type queryType = typeof(IQueryable<TModel>);
                Type funcType = typeof(Expression<Func<TModel, bool>>);
                Type valuesType = typeof(string[]);

                ParameterExpression typeExpression = Expression.Parameter(entityType, "entity");
                ParameterExpression queryExpression = Expression.Parameter(queryType, "euery");
                ParameterExpression valuesExpression = Expression.Parameter(valuesType, "values");

                Expression<Func<TModel, bool>> funcExpression = BuildWhereFuncExpression(typeExpression, valuesExpression,
                    filters);

                Type[] typeArgs = new Type[] { entityType };
                MethodCallExpression whereExpression =
                    Expression.Call(typeof(Queryable), "Where", typeArgs, queryExpression, funcExpression);


                return Expression.Lambda<Func<IQueryable<TModel>, string[], IQueryable<TModel>>>(whereExpression, queryExpression, valuesExpression);
            }
            private static Expression<Func<TModel, bool>> BuildWhereFuncExpression(ParameterExpression typeExpression,
                ParameterExpression valuesExpression, IEnumerable<FilterItemExpression> filters)
            {
                Expression funcExprssion = null;
                foreach (var filter in filters)
                {
                    Expression filterExpression =
                        BuildFilterExpression(typeExpression, valuesExpression, filter);

                    if (funcExprssion == null)
                    {
                        funcExprssion = filterExpression;
                    }
                    else
                    {
                        funcExprssion = Expression.AndAlso(funcExprssion, filterExpression);
                    }
                }

                return Expression.Lambda<Func<TModel, bool>>(funcExprssion, typeExpression);
            }

            private static readonly MethodInfo BuildLeDateTimeMethod = typeof(QueryableWhereExtensions).GetMethod("BuildLeDateTime", BindingFlags.NonPublic | BindingFlags.Static);
            private static Expression BuildFilterExpression(ParameterExpression typeExpression,
                Expression valuesExpression, FilterItemExpression filter)
            {
                Expression propertyExpression = PropertyDescriptor.BuildPropertyExpression(typeExpression, filter.Name);

                Expression valueExpression = filter.BuildValueExpression(valuesExpression, propertyExpression.Type);

                switch (filter.Operation)
                {
                    case FilterOperation.Eq:
                        return Expression.Equal(propertyExpression, valueExpression);
                    case FilterOperation.NotEq:
                        return Expression.NotEqual(propertyExpression, valueExpression);
                    case FilterOperation.Like:

                        return Expression.Call(propertyExpression,
                               typeof(string).GetMethod("Contains", new Type[] { typeof(string) }),
                               valueExpression);
                    case FilterOperation.NotLike:

                        return Expression.Not(Expression.Call(propertyExpression,
                               typeof(string).GetMethod("Contains", new Type[] { typeof(string) }),
                               valueExpression));

                    case FilterOperation.Le:
                        return Expression.LessThan(propertyExpression, valueExpression);
                    case FilterOperation.LeEq:
                        if (propertyExpression.Type == typeof(DateTime))
                        {
                            return Expression.LessThan(propertyExpression,
                               Expression.Call(BuildLeDateTimeMethod, valueExpression));
                        }
                        return Expression.LessThanOrEqual(propertyExpression, valueExpression);
                    case FilterOperation.Gt:
                        return Expression.GreaterThan(propertyExpression, valueExpression);
                    case FilterOperation.GtEq:

                        return Expression.GreaterThanOrEqual(propertyExpression, valueExpression);

                    default:
                        throw new NotSupportedException(filter.Operation.ToString());
                }
            }
        }
        private static DateTime BuildLeDateTime(DateTime old)
        {
            if (old.Hour == 0 && old.Minute == 0 && old.Second == 0 && old.Millisecond == 0)
            {
                return old.AddDays(1);
            }
            else if (old.Minute == 0 && old.Second == 0 && old.Millisecond == 0)
            {
                return old.AddHours(1);
            }
            else if (old.Second == 0 && old.Millisecond == 0)
            {
                return old.AddMinutes(1);
            }
            else if (old.Millisecond == 0)
            {
                return old.AddSeconds(1);
            }
            else
            {
                return old;
            }
        }

    }
}
